Skip to content

feat(ia): 备考域重布局 + catalog/成绩第一刀 - #25

Merged
ROM4n2 merged 12 commits into
masterfrom
feat/ia-nav-exam-domain
Sep 4, 2026
Merged

feat(ia): 备考域重布局 + catalog/成绩第一刀#25
ROM4n2 merged 12 commits into
masterfrom
feat/ia-nav-exam-domain

Conversation

@ROM4n2

@ROM4n2 ROM4n2 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

feat(ia): 备考域重布局 + catalog/成绩第一刀(ADR-0005 Phase 1)

按 ADR-0005(d:/Obsidian/Coding/08-Projects/DeLector/01-ADR/0005-navigation-exam-domain-and-level-scalability.md)把 DeLector 前端从「工具混入考纲素材」重排为「场景工具 + 独立备考域」,并落地目录/成绩泛化第一刀。

关键决策(vault-grill 拍板)

  • 备考域收拢 五模块全迁(含 A1 词表,不只听/读/口语)
  • 主导航静态加按钮(nav.js 单源重渲染取消——YAGNI,等级页签本就 catalog 驱动)
  • 题库不入 SQLite(exam_catalog.py 代码注册目录单源,加级=插一行)

变更

  • Task 1 备考域骨架 + 静态双端导航入口
  • Task 2 A1 五模块迁入 view-exam(写作/听力/阅读/口语/词表);view-writer 回归纯 essay、view-cards 回归纯复习;新增 tools/ia_dom_mount_probe.mjs 行为级 DOM 探针(node:vm 真跑,回退必红)
  • Task 3 /api/exams/catalog 目录化端点到前端数据驱动
  • Task 4 exam_trials(level,module,…) 泛化成绩表 + 幂等迁移 A1 存量 + 备份/还原接线;旧 a1_hoeren_records/a1_lesen_records 保留兼容期
  • Task 5 全量回归 + ledger + README(v5.2.0)

测试证据

  • 全量 pytest 559 passed(基线 518,+41)
  • 10/10 tools/*.mjs 探针全绿(新增 ia_dom_mount_probe)
  • 每 Task RED→Verify Red→GREEN 证据 + 独立评审子代理(Maker-Checker)逐项 PASS
  • 评审期间发现并修复一处真缺陷:migrate 幂等谓词 ==>=(透传模式「迁移后又做成绩再重启」会整表重插,已复现 count 3→5,回归测试钉死)

范围外(后继触发点,见计划 §9)

  • 听/读组件 level 参数化、写作判分提取、/api/a1/* 全量切 /api/exams/{level}/{module}、旧 a1 端点/表退役 → A2 立项时
  • 背词工作台 token 壳/词表契约 → 独立 short-plan

计划 / ledger

  • 计划:docs/plans/2026-09-04-ia-nav-exam-domain-phase1.md
  • Ledger:docs/plans/2026-09-04-ia-nav-exam-domain-phase1-ledger.md

- test_audit_hardening: clean_db fixture 前后钉 DATABASE_PATH/PROGRESS_DB_PATH,
  防全量 pytest 时 test_server.py 顶层改写 env 导致本文件命中未建表的默认库。
- test_server: import server 改 import nlp(9fea63e 已剔除 server 侧 nlp 透传,
  旧引用是死测路径)。
- docs(plans): ADR-0005 Phase1 实施计划入库(词表全迁/静态导航/题库不入库三决策版)。
- 桌面 nav + 移动 dock 静态加「备考 (PRÜFUNG)」双端入口(nav-btn-exam/mob-btn-exam)。
- 新建 view-exam 壳:exam-level-tabs(A1 占位) + exam-module-grid 五模块占位卡片
  (写作/听力/阅读/口语/词表)——置于 view-german 之前不破 wb split 切块断言。
- style.css 新增 .exam-* 最小样式(复用既有 paper 体系变量,纯插入)。
- show('exam') 零 JS 改动(main.js view-id 惯例天然点亮)。
- test_exam_domain.py 11 条契约测试(含顺序/配平/死测防恒真切片)。
- 定向回归 128 passed(基线 117 + 新增 11)。
- view-exam 收拢五面板:写作(exam-writing + setExamWritingTab 页签)、
  词表/口语(exam-cards-container + 自管 toggle)、听力/阅读(原容器 id 搬入)。
- view-writer 回归纯 essay(writer-mode-a1-* 删除,switchWriterMode 收敛 no-op 防悬空);
  view-cards 回归纯复习四段(seg-a1 删除、renderCardsGrid a1 分流删净)。
- a1_cards.js 渲染目标 a1CardsHost() 改指 exam 容器(回退主站 cards-container 即探针红);
  setA1Mode 加冷缓存懒加载守卫(等待期 mode 竞态防护)。
- main.js show() 守卫改 view !== "exam"(停考计时器)、exam 分支 loadCards +
  setExamModule mediator(页签⇄面板路由);exposer 补挂 3 函数(含 import 链核对)。
- 新探针 tools/ia_dom_mount_probe.mjs(node:vm 真跑 4 场景,含变异验证 7 条回退必红)。
- 测试同步:seg-a1→exam-card-vocab、writer-mode-a1-*→exam-tab-*(ledger 登记项)。
- 回归 537 passed(基线 518+11+8)+ 10/10 探针全绿;评审 PASS(悬空标识符/双现/探针质量核查)。
- exam_catalog.py 单源注册目录(EXAM_CATALOG,count_fn 数据推导:writing 18/
  hoeren 5/lesen 6/sprechen 54/vocab 702),_safe_count 抛错记 0 + logger.warning
  留痕(零静默吞异常,caplog 钉死);docstring 如实标注模块级 import 硬依赖面。
- routes_exam.py GET /api/exams/catalog 纯只读端点;server.py include 一行接入。
- main.js initExamCatalog:fetch catalog → 卡片标题/count 徽标/新等级页签;
  失败静态回退 + 幂等守卫 + show('exam') 惰性触发;非静态新页签标注「待接入」
  (no-op + aria-disabled,防死按钮 v5.1.0 前科)。
- 加级成本已验证:插一行 A2 → catalog 多一级(变异断言钉死扩展点)。
- 旧 /api/a1/* 端点零改动(6 端点 smoke 锚);定向回归 37 passed。
- 评审 PASS(初轮 REVISE 两处已修:吞异常 + 死代码)。
- progress.db 新增 exam_trials(level,module,set_id,score_raw,…) 泛化成绩表;
  record_exam_trial/get_exam_history 泛化读写(9 列投影,无 level/module 冗余泄漏)。
- 旧 record_a1_hoeren_trial/record_a1_lesen_trial/get_a1_hoeren_history/get_a1_lesen_history
  改内部透传(签名+返回结构不变,调用方零改动)。
- migrate_a1_records_to_exam_trials() 幂等迁移:行数对账谓词用 >=(非 ==)——
  == 在「透传后旧表冻结+新表随成绩单调增,迁移后又做一次成绩再重启」场景
  general=legacy+1 误判未迁移→整表重插(已复现 count 3→5,回归测试钉死)。
  db_progress_conn 单事务保证半迁回滚;migrate 每次启动跑,增/幂等皆安全。
- 备份接线:_PROGRESS_TABLES 带 exam_trials + RestoreReq.exam_trials 字段 +
  restore 灌表 + restore 后幂等迁移(v5.1 旧备份无 exam_trials 键时历史仍可见)。
- test_server.py clean_db env 双钉(防收集顺序导致未建表库命中)。
- 回归 287 passed(含新增 11 条 exam_trials 契约测试 + 迁移重入回归);
  评审 PASS(幂等/透传契约/init 无递归/备份链/SQL 参数化逐项核验)。
Copilot AI lite review requested due to automatic review settings September 4, 2026 11:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are confirmed functional and data-integrity issues in the changed code (HTML markup breakage, exam-domain A1 toolbar visibility, and incorrect A1 event_type mapping) that should be fixed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR implements ADR-0005 Phase 1 by introducing an explicit “Exam Domain” (备考域) in the frontend navigation, moving Goethe A1 learning modules out of tool views into that domain, and adding the first “catalog + generalized trials” backend primitives to make levels/modules scalable.

Changes:

  • Frontend: add view-exam and new nav/dock entry; migrate A1 writing/listening/reading/speaking/vocab UI to the exam domain with a root-module mediator (setExamModule) and a DOM+behavior probe (ia_dom_mount_probe.mjs).
  • Backend: add /api/exams/catalog driven by a code-registered catalog (exam_catalog.py) and generalize A1 exam results into exam_trials with idempotent migration + backup/restore wiring.
  • Tests/docs: substantial new regression coverage for exam-domain DOM wiring, catalog contract, trials migration/backup, plus README + plan/ledger updates.
File summaries
File Description
tools/ia_dom_mount_probe.mjs New node:vm behavioral probe to ensure migrated A1 panels mount into view-exam and render into the correct containers.
test_server.py Hardens DB env isolation in fixtures; updates NLP tests to import from nlp instead of server.
test_goethe_a1.py Updates A1 frontend DOM assertions to match the new exam-domain entry point.
test_goethe_a1_writing.py Updates writing-workshop DOM assertions for the exam-domain tabs (exam-tab-*).
test_exam_trials.py New test suite for generalized exam_trials, legacy pass-through, idempotent migration, and backup/restore wiring.
test_exam_domain.py New static + dynamic (probe-driven) contract tests for the exam-domain nav/view structure and routing.
test_exam_catalog.py New tests for /api/exams/catalog contract, extension behavior, failure resilience, and legacy /api/a1/* smoke.
test_audit_hardening.py Mirrors the “pin env per test” fixture discipline to avoid collection-order DB contamination.
static/style.css Adds minimal styling for the new exam-domain layout (level tabs, module card grid, count badge).
static/js/writer.js Re-exports setExamWritingTab via a1_writer.js for the new exam-domain mediator wiring.
static/js/main.js Updates show() for exam-domain lifecycle; adds setExamModule mediator; adds catalog lazy init and UI enrichment.
static/js/cards.js Removes the a1 segment handling from cards view (A1 now owned by exam domain).
static/js/a1_writer.js Refactors A1 writing switching into setExamWritingTab; keeps switchWriterMode() as a no-op to avoid dangling references.
static/js/a1_cards.js Redirects A1 rendering to #exam-cards-container; adds exam-local view-mode state + lazy-load guard.
static/index.html Adds nav/dock “Exam” entry and new view-exam; moves A1 module DOM into exam panels; removes legacy A1 UI from tool views.
server.py Includes the new exam router; extends restore payload with exam_trials and triggers post-restore A1→exam_trials migration.
routes_exam.py New router providing /api/exams/catalog.
README.md Updates test badge count and documents ADR-0005 Phase 1 changes for v5.2.0.
exam_catalog.py New code-registered exam catalog with safe count derivation and warning logging on failures.
docs/plans/2026-09-04-ia-nav-exam-domain-phase1.md New/updated implementation plan document for ADR-0005 Phase 1.
docs/plans/2026-09-04-ia-nav-exam-domain-phase1-ledger.md New/updated execution ledger with verification evidence and deltas.
database.py Adds exam_trials table, generalized record/history helpers, idempotent A1 migration, and backup table spec wiring.
Review details
  • Files reviewed: 22/22 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread database.py
Comment on lines +951 to +954
if level == "A1":
event_type = "a1_hoeren" if module == "hoeren" else "a1_lesen"
else:
event_type = f"{level.lower()}_{module}"
Comment thread static/index.html
Comment on lines +2635 to +2641
<a
href="/api/a1/export/anki"
class="btn btn-ghost btn-xs a1-anki-export-btn"
title="一键导出官方歌德 A1 词表到 Anki 牌组"
>📥 导出 A1 牌组</a
>
</div>
Comment thread static/js/a1_cards.js
Comment on lines 109 to 113
const searchRow = document.getElementById("a1-search-row");
const pillsRow = document.getElementById("a1-topic-pills");
const viewToggle = document.querySelector(".cards-view-toggle");
const cardsContainer = document.getElementById("cards-container");
const viewToggle = document.getElementById("exam-cards-view-toggle");
const cardsContainer = a1CardsHost();
const hoerenContainer = document.getElementById("a1-hoeren-container");
Comment thread static/js/main.js
Comment on lines +319 to +323
btn.textContent = lv.title || lv.id;
btn.title = "该等级模块待接入";
btn.setAttribute("aria-disabled", "true");
btn.onclick = () => {}; // no-op:防静默死按钮,待该等级模块接线后替换
tabs.appendChild(btn);
@ROM4n2
ROM4n2 merged commit 8e2ad85 into master Sep 4, 2026
1 check passed
@ROM4n2
ROM4n2 deleted the feat/ia-nav-exam-domain branch September 4, 2026 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants